## 1. Объявите в программе базовый класс Animal (животное), объекты которого можно создать командой:

### an = Animal(name, old)

где `name` - название животного (строка);

 `old` - возраст животного (целое число). 

Такие же локальные атрибуты (`name и old`) должны создаваться в объектах класса.

Далее, объявите дочерний класс (от базового Animal) с именем Cat (кошки), объекты которого создаются командой:
```
cat = Cat(name, old, color, weight)
```
где `name, old` - те же самые параметры, что и в базовом классе; 

`color` - цвет кошки (строка); 

`weight` - вес кошки (любое положительное число).

В объектах класса `Cat` должны автоматически формироваться локальные атрибуты: `name, old, color, weight`. 

Формирование атрибутов `name, old` должен выполнять инициализатор базового класса. 

По аналогии объявите еще один дочерний класс `Dog` (собака), объекты которого создаются командой:
```
dog = Dog(name, old, breed, size)
```
здесь` name, old` - те же самые параметры, что и в базовом классе; 

`breed` - порода собаки (строка); 

`size` - кортеж в формате (`height, length`) высота и длина - числа.

В объектах класса `Dog` по аналогии должны формироваться локальные атрибуты: `name, old, breed, size`. 

За формирование атрибутов `name, old` отвечает инициализатор базового класса. 

Наконец, в классах `Cat и Dog` объявите метод:
```python
get_info() - для получения информации о животном.
```
Этот метод должен возвращать строку в формате:
```
"name: old, <остальные параметры через запятую>"
```
Например, для следующего объекта класса Cat:
```python
cat = Cat('кот', 4, 'black', 2.25)
```
метод get_info должен вернуть строку:
```
"кот: 4, black, 2.25"
```

<details>
<summary>Подсказка</summary>

```python
class Animal:
    def __init__(self, name, old):
        self.name = name
        self.old = old

    def get_info(self):
        return f"{self.name}: {', '.join(map(str, list(self.__dict__.values())[1:]))}"


class Cat(Animal):
    def __init__(self, name, old, color, weight):
        super().__init__(name, old)
        self.color = color
        self.weight = weight


class Dog(Animal):
    def __init__(self, name, old, breed, size=()):
        super().__init__(name, old)
        self.breed = breed
        self.size = size
```
</details>


## 2. вы разрабатываете программу для интернет-магазина. 

В этом магазине могут быть как реальные (физические) товары, так и электронные. 

Для этих двух групп, очевидно, нужен разный набор атрибутов:

> для реальных физических товаров: id, name, price, weight, dims

где `id` - идентификатор товара (целое число);

`name` - наименование товара (строка);

`price` - цена товара (вещественное число); 

`weight` - вес товара (вещественное число);

`dims = (lenght, width, depth)` - длина, ширина, глубина - габариты товара (вещественные числа);

> для электронных товаров: `id, name, price, memory, frm`

где `id` - идентификатор товара (целое число);

`name` - наименование товара (строка); 

`price` - цена товара (вещественное число); 

`memory` - занимаемый размер (в байтах - целое число);

`frm` - формат данных (строка: pdf, docx и т.п.)

Так как все товары могут идти вперемешку, то мы хотим, чтобы в каждом объекте (для товара) присутствовали все атрибуты:

`id, name, price, weight, dims, memory, frm`

с начальными значениями `None`. 

А уже, затем, нужным из них будут присвоены конкретные данные.

Для реализации этой логики объявите в программе базовый класс с именем `Thing` (вещь, предмет), объекты которого могут создаваться командой:

```python
th = Thing(name, price)
```
А атрибут `id` должен формироваться автоматически и быть уникальным для каждого товара (например, можно для каждого нового объекта увеличивать на единицу).

В объектах класса `Thing` должен формироваться полный набор локальных атрибутов (`id, name, price, weight, dims, memory, frm`) со значением `None`, кроме атрибутов: `id, name, price`.

Далее, нужно объявить два дочерних класса:

```
Table - для столов;
ElBook - для электронных книг.
```
Объекты этих классов должны создаваться командами:

```python
table = Table(name, price, weight, dims)
book = ElBook(name, price, memory, frm)
```

Причем, атрибуты `name, price` (а также `id`) следует инициализировать в базовом классе, т.к. они общие для всех товаров. 

Остальные атрибуты должны либо принимать значение `None`, если не используются, либо инициализироваться конкретными значениями уже в дочерних классах.

Наконец, в базовом классе `Thing` объявите метод:

`get_data()` - для получения кортежа в формате (`id, name, price, weight, dims, memory, frm`)

Пример использования классов :

```python
table = Table("Круглый", 1024, 812.55, (700, 750, 700))
book = ElBook("Python ООП", 2000, 2048, 'pdf')
print(*table.get_data())
print(*book.get_data())
```


<details>
<summary>Подсказка</summary>

```python

class Thing:
    i = 0

    def __init__(self, name, price):
        Thing.i += 1
        self.id = self.i
        self.name = name
        self.price = price
        self.weight = None
        self.dims = None
        self.memory = None
        self.frm = None

    def get_data(self):
        return tuple(self.__dict__.values())


class Table(Thing):
    def __init__(self, name, price, weight, dims):
        super().__init__(name, price)
        self.weight = weight
        self.dims = dims


class ElBook(Thing):

    def __init__(self, name, price, memory, frm):
        super().__init__(name, price)
        self.memory = memory
        self.frm = frm


### или так

class Thing:
    id = 0

    def __init__(self, *args):
        self.id = Thing.id
        self.name, self.price, self.weight, self.dims, self.memory, self.frm = args
        Thing.id += 1

    def get_data(self):
        return tuple(self.__dict__.values())


class Table(Thing):

    def __init__(self, name, price, weight, dims, memory=None, frm=None):
        super().__init__(name, price, weight, dims, memory, frm)


class ElBook(Thing):

    def __init__(self, name, price, memory, frm, weight=None, dims=None):
        super().__init__(name, price, weight, dims, memory, frm)

```
</details>

## 3. Создайте класс User и и его наследника класс SuperUser, которые описывают пользователя и супер-пользователя

> В классе `User` необходимо описать:
*	конструктор, который принимает в качестве параметров значения для атрибутов `name, login и password`

*	свойства для изменения и получения значений атрибутов

*	метод `show_info`, который печатает в произвольном формате значения атрибутов `name` и `login`

*	атрибут класса count для хранения количества созданных экземпляров класса `User`

> Необходимые условия, которые надо учесть:

После создания объекта

*	атрибут name доступен и для чтения, и для изменения
*	атрибут login доступен только для чтения
*	атрибут password доступен только для изменения

> В классе SuperUser необходимо описать:

*	конструктор, который принимает в качестве параметров значения для атрибутов name, login, password и role
*	свойство для изменения и получения значения атрибута role
*	метод show_info, который печатает в произвольном формате значения атрибутов name, login и role
•	атрибут класса count для хранения количества созданных экземпляров класса SuperUser

Как это должно работать
```python
user1 = User('Paul McCartney', 'paul', '1234')
user2 = User('George Harrison', 'george', '5678')
user3 = User('Richard Starkey', 'ringo', '8523')
admin = User('John Lennon', 'john', '0000', 'admin')

user1.show_info() # Например: Name: Paul McCartney, Login: paul
admin.show_info() # Например: Name: John Lennon, Login: john

users = User.count
admins = SuperUser.count

print(f'Всего обычных пользователей: {users}') # Всего обычных пользователей: 3
print(f'Всего супер-пользователей: {admins}') # Всего супер-пользователей: 1

user3.name = 'Ringo Star'
print(user3.name) # Ringo Starr

print(user2.login) # george
user2.login = 'geo' # Должна быть ошибка

user1.password = 'Pa$$w0rd'
print(user2.password) # Должна быть ошибка
```




## Тестовые значение

```python
user1 = User('Paul McCartney', 'paul', '1234')
user2 = User('George Harrison', 'george', '5678')
user3 = User('Richard Starkey', 'ringo', '8523')
admin = SuperUser('John Lennon', 'john', '0000', 'admin')

user1.show_info() # Например: Name: Paul McCartney, Login: paul
admin.show_info() # Например: Name: John Lennon, Login: john

users = User.count
admins = SuperUser.count

print(f'Всего обычных пользователей: {users}') # Всего обычных пользователей: 3
print(f'Всего супер-пользователей: {admins}') # Всего супер-пользователей: 1

user3.name = 'Ringo Star'
print(user3.name) # Ringo Starr

print(user2.login) # george
# user2.login = 'geo' # Должна быть ошибка

user1.password = 'Pa$$w0rd'
# print(user2.password) # Должна быть ошибка
```
> Результат:
```
Name: Paul McCartney
Login: paul
Name: John Lennon
Login: john
Role: admin
Всего обычных пользователей: 4
Всего супер-пользователей: 1 
Ringo Star
george
```



<details>
<summary>Решение </summary>

```python
class User:
    count = 0

    def __init__(self, n, l, p):
        self.__n = n
        self.__l = l
        self.__p = p
        User.count += 1

    def get_name(self):
        return self.__n

    def set_name(self, n):
        self.__n = n

    name = property(get_name, set_name)

    def get_login(self):
        return self.__l

    def set_login(self, value):
        raise AttributeError('Нельзя менять значение')

    login = property(get_login, set_login)

    def get_passw(self):
        return '******'

    def set_passw(self, value):
        self.__p = value

    password = property(get_passw, set_passw)

    def show_info(self):
        print(f'Name: {self.__n}')
        print(f'Login: {self.__l}')


class SuperUser(User):
    count = 0

    def __init__(self, n, l, p, r):
        super().__init__(n, l, p)
        self.__role = r
        SuperUser.count +=1
    @property
    def role(self):
        return self.__role
    @role.setter
    def role(self, value):
        self.__role = value

    def show_info(self):
        super().show_info()
        print(f'Role: {self.__role}')


u = User(1, 2, 3)
u.show_info()

s1 = SuperUser(1,2,3,4)
s1.show_info()

Результат:
Name: 1
Login: 2
Name: 1
Login: 2
Role: 4

```
</details>


## Вопрос: Как банки воруют миллионы? Где ошибка
Засада `count` считает в двух местах

Мы должны СуперЮзера вычесть этот count обратно
```python
def __init__(self, n, l, p, r):
    super().__init__(n, l, p)
    self.__role = r
    SuperUser.count += 1
    super().count -= 1
```

Работает. Напоминаю как работает супер
```python
super().count -= 1 # ---> User.count-=1
```

Проверим работу:
Всего обычных пользователей: 3
Всего супер-пользователей: 1
или более правильно динамические обратиться через магию `__class`__

```python
self.__class__.count -= 1 #так не работает!!! Не показывать
```


### 4. браузер (и не только) может отправлять на сервер различные типы запросов: GET, POST, PUT, DELETE и др. 

Каждый из этих типов запросов обрабатывается в программе на сервере своим отдельным методом. Чтобы каждый раз не прописывать все необходимые методы в классах при обработке входящих запросов, они выносятся в базовый класс и вызываются из дочерних. Выполним такой пример.

Пусть в программе объявлен следующий базовый класс с именем GenericView:

```python
class GenericView:
    def __init__(self, methods=('GET',)):
        self.methods = methods

    def get(self, request):
        return ""

    def post(self, request):
        pass

    def put(self, request):
        pass

    def delete(self, request):
        pass
```

Здесь каждый метод отвечает за обработку своего типа запроса. 

Параметр methods - это кортеж или список, состоящий из набора разрешенных запросов: строк с именами соответствующих методов (как правило, пишут заглавными буквами).

Вам необходимо объявить дочерний класс с именем `DetailView, объекты которого можно создавать командами:

```python
dv = DetailView()  # по умолчанию methods=('GET',)
dv = DetailView(methods=('PUT', 'POST'))
```
Для инициализации атрибута methods следует вызывать инициализатор базового класса `GenericView`.

Далее, в классе `DetailView` нужно определить метод:

```python
def render_request(self, request, method): ...
```

который бы имитировал выполнение поступившего на сервер запроса. 

Здесь
> request - словарь с набором данных запроса; 

> method - тип запроса (строка: 'get' или 'post' и т.д.).

Например:
```python
html = dv.render_request({'url': 'https://site.ru/home'}, 'GET')
```

должен быть обработан запрос как GET-запрос с параметром url и значением 'https://site.ru/home'. 

Параметр url является обязательным в словаре request для каждого запроса.

В методе `render_request()` необходимо выполнить проверку: является ли указанный метод `(method)` разрешенным (присутствует в коллекции `methods`). 

Если это не так, то генерировать исключение командой:
```python
raise TypeError('данный запрос не может быть выполнен')
```
Если проверка проходит, то выполнить соответствующий метод (или `get()`, или `post()`, или `put()` и т.д. с возвращением результата их работы). 

Подсказка: для получения ссылки на нужный метод можно воспользоваться магическим методом `__getattribute__()` или аналогичной функцией `getattr())`.

Наконец, в дочернем классе `DetailView` следует переопределить метод `get()` для нужной нам обработки `GET-запросов`. 

В этом методе нужно выполнить проверку, что параметр `request` является словарем. 

Если это не так, то генерировать исключение:
```python
raise TypeError('request не является словарем')
```

Сделать проверку, что в словаре request присутствует ключ `url`. 

Если его нет, то генерировать исключение:

```python
raise TypeError('request не содержит обязательного ключа url')
```
Если же все проверки проходят, то вернуть строку в формате:

```python
"url: <request['url']>"
```
Пример:

```python
dv = DetailView()
html = dv.render_request({'url': 'https://site.ru/home'}, 'GET')   # url: https://site.ru/home
```


<details>
<summary>Решение </summary>

```python
class GenericView:
    def __init__(self, methods=('GET',)):
        self.methods = methods

    def get(self, request):
        return ""

    def post(self, request):
        pass

    def put(self, request):
        pass

    def delete(self, request):
        pass


class DetailView(GenericView):
    def __init__(self, methods=('GET',)):
        super().__init__(methods)

    def render_request(self, request, method):
        if method not in self.methods:
            raise TypeError('данный запрос не может быть выполнен')
        getattr(self, method.lower())(request)


    def get(self, request):
        if type(request) != dict:
            raise TypeError('request не является словарем')
        if not 'url' in request:
            raise TypeError('request не содержит обязательного ключа url')
        print(f"url: {request['url']}")
        return f"url: {request['url']}"


dv1 = DetailView()  # по умолчанию methods=('GET',)
dv2 = DetailView(methods=('PUT', 'POST'))
print(dv2.methods)
print(dv1.methods)

html = dv1.render_request({'url': 'https://site.ru/home'}, 'GET')


```
</details>



### 6.  Объявите в программе класс с именем Singleton

который бы позволял создавать только один экземпляр (все последующие экземпляры должны ссылаться на первый). 

Затем, объявите еще один класс с именем:

> Game

который бы наследовался от класса `Singleton`. 

Объекты класса `Game` должны создаваться командой:
```python
game = Game(name)
```
где `name` - название игры (строка).

В каждом объекте класса `Game` должен создаваться атрибут `name` с соответствующим содержимым.

Убедитесь, что атрибут `name` принимает значение первого созданного объекта (если это не так, то поправьте инициализатор дочернего класса, чтобы это условие выполнялось).




<details>
<summary>Решение </summary>

```python
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = object.__new__(cls)
        return cls._instance


class Game(Singleton):

    def __init__(self, name):
        if 'name' not in self.__dict__:
            self.name = name
```
</details>



### Создается проект, в котором предполагается использовать списки из целых чисел. 

Для этого вам ставится задача создать класс с именем ListInteger с базовым классом list и переопределить три метода:

```python
__init__()
__setitem__()
append()
```

так, чтобы список `ListInteger` содержал только целые числа. 

При попытке присвоить любой другой тип данных, генерировать исключение командой:

```python
raise TypeError('можно передавать только целочисленные значения')
```

Пример использования класса ListInteger 


```python
s = ListInteger((1, 2, 3))
s[1] = 10
s.append(11)
print(s)
s[0] = 10.5 # TypeError
```


<details>
<summary>Решение </summary>

```python
class ListInteger(list):

    def __init__(self, obj):
        super().__init__(obj)

    def append(self, value):
        self.__check(value)
        super().append(value)

    def __setitem__(self, key, value):
        self.__check(value)
        super().__setitem__(key, value)

    @staticmethod
    def __check(*args):
        for arg in args:
            if not isinstance(arg, int):
                raise TypeError('можно передавать только целочисленные значения')
```
</details>